import { Lucia, TimeSpan, generateIdFromEntropySize, generateId } from "lucia";
import { D1Adapter } from "@lucia-auth/adapter-sqlite";
import { getD1DataByTable, insertD1Data } from "./d1-data";
import { validateScryptHash, generateScryptHash } from "./scrypt";

export type Role = "admin" | "editor" | "user";
export type Plan = "normal" | "pro" | "supper";

export interface User {
	id: string;
	username: string;
	email: string;
	email_verified: 0 | 1;
	phone: string;
	phon_verified: 0 | 1;
	password_hash: string;
	push_wx: string;
	push_tg: string;
	push_web: string;
	role: Role;
	plan: Plan;
}

export interface UserErr {
	code: 401 | 403;
	message: string;
}

export interface UserResp {
	code: 200;
	user: User;
	session: string;
}

export function initializeLucia(D1: D1Database) {
	const adapter = new D1Adapter(D1, {
		user: "users",
		session: "user_sessions",
	});
	return new Lucia(adapter, {
		sessionExpiresIn: new TimeSpan(2, "w"), // 2 weeks
		sessionCookie: {
			attributes: {
				secure: false,
			},
		},
	});
}

export async function hashPassword(
	password: string,
	kdf: Env["AUTH_KDF"] = "pbkdf2",
	salt: string,
	iterations = 100000,
	hash = "SHA-512",
) {
	password = password.normalize("NFKC");
	kdf = kdf.toLowerCase() as Env["AUTH_KDF"];
	if (kdf !== "pbkdf2" && kdf !== "scrypt") {
		kdf = "pbkdf2";
	}
	hash = hash.toUpperCase();
	if (hash !== "SHA-512" && hash !== "SHA-384" && hash !== "SHA-256") {
		hash = "SHA-512";
	}
	if (kdf === "pbkdf2") {
		const encoder = new TextEncoder();
		const passwordBuffer = encoder.encode(password);
		const baseKey = await crypto.subtle.importKey(
			"raw",
			passwordBuffer,
			{ name: "PBKDF2" },
			false,
			["deriveBits"],
		);
		let dkLen = 64;
		switch (hash) {
			case "SHA-384":
				dkLen = 48;
				break;
			case "SHA-256":
				dkLen = 32;
				break;
		}
		const hashedPassword = await crypto.subtle.deriveBits(
			{
				name: "PBKDF2",
				salt: encoder.encode(salt),
				iterations,
				hash,
			},
			baseKey,
			dkLen * 8,
		);

		return {
			kdf,
			hash,
			salt,
			iterations,
			hashedPassword: String.fromCharCode.apply(
				null,
				// @ts-ignore
				new Uint8Array(hashedPassword),
			),
		};
	} else {
		return {
			kdf,
			hash,
			salt,
			iterations,
			hashedPassword: await generateScryptHash(password),
		};
	}
	// let uint8Array = new Uint8Array(exportedKey);
	// return String.fromCharCode.apply(null, uint8Array);
}

function getIterations(iterationsString?: string) {
	let iterations = 100000;
	if (iterationsString) {
		try {
			iterations = +iterationsString;
		} catch (e) {
			console.error("failed to parse AUTH_ITERATIONS", e);
		}
	}
	return Math.min(iterations, 100000);
}

const constantTimeEqual = (a: string, b: string): boolean => {
	if (a.length !== b.length) {
		return false;
	}
	const aUint8Array = new TextEncoder().encode(a);
	const bUint8Array = new TextEncoder().encode(b);

	let c = 0;
	for (let i = 0; i < a.length; i++) {
		c |= aUint8Array[i] ^ bUint8Array[i]; // ^: XOR operator
	}
	return c === 0;
};

const passwordHash = async (userPassword: string, env: Env) => {
	const salt = generateId(16);

	const hash = await hashPassword(
		userPassword,
		env.AUTH_KDF as Env["AUTH_KDF"],
		salt,
		getIterations(env.AUTH_ITERATIONS),
		env.AUTH_HASH,
	);
	if (hash.kdf === "pbkdf2") {
		return `snc:$:${hash.hashedPassword}:$:${salt}:$:${hash.hash}:$:${hash.iterations}`;
	} else {
		return `lca:$:${hash.hashedPassword}`;
	}
};
const passwordVerify = async (userPassword: string, dbHash: string) => {
	const [hasher, hashedPassword, salt, hash, iterations] = dbHash.split(":$:");

	let kdf: Env["AUTH_KDF"] = "pbkdf2";
	if (hasher === "lca") {
		kdf = "scrypt";
	}
	if (kdf === "pbkdf2") {
		const verifyHash = await hashPassword(
			userPassword,
			kdf,
			salt,
			getIterations(iterations),
			hash,
		);
		return constantTimeEqual(verifyHash.hashedPassword, hashedPassword);
	} else {
		return await validateScryptHash(userPassword, dbHash.substring(4));
	}
};

export interface ILoginUser {
	email: string;
	password: string;
}
export const login = async (
	d1: D1Database,
	params: ILoginUser,
): Promise<UserErr | UserResp> => {
	const { email, password } = params;
	const d1Data = (await getD1DataByTable(d1, "users", {
		email,
	})) as unknown as null | User;
	console.log(d1Data);
	if (!d1Data) {
		return {
			code: 401,
			message: "not found user",
		};
	} else {
		const verified = await passwordVerify(
			password,
			d1Data.password_hash as string,
		);
		if (!verified) {
			return {
				code: 401,
				message: "password not match",
			};
		} else {
			const lucia = initializeLucia(d1);
      await lucia.deleteExpiredSessions();
			const session = await lucia.createSession(d1Data.id, {});
			return {
				code: 200,
				session: session.id,
				user: {
					...d1Data,
					password_hash: "",
				},
			};
		}
	}
};

export function isValidEmail(email: string): boolean {
	return /.+@.+/.test(email);
}

export interface IRegisterUser {
	username: string;
	email: string;
	password: string;
	confirmPassword: string;
}
export const register = async (
	env: Env,
	params: IRegisterUser,
): Promise<UserErr | UserResp> => {
	const { email, password, confirmPassword, username } = params;
	if (password.length < 6 || password.length > 255) {
		return {
			code: 401,
			message: "password not match 1",
		};
	}
	if (!isValidEmail(email)) {
		return {
			code: 401,
			message: "email not match",
		};
	}
	if (password !== confirmPassword) {
		return {
			code: 401,
			message: "password not match",
		};
	}
	const d1 = env.D1DATA;
	const d1Data = await getD1DataByTable(d1, "users", {
		email,
	});
	console.log("register d1Data:", d1Data);
	if (d1Data) {
		return {
			code: 401,
			message: "found user",
		};
	} else {
		const hashPw = await passwordHash(password, env);
		const userId = generateIdFromEntropySize(10);
		const d1Data = (await insertD1Data(d1, "users", {
			id: userId,
			password_hash: hashPw,
			email,
			username,
			role: env.ENVIRONMENT === "development" ? "admin" : "user", // TODO: 开发环境为管理员
			plan: "normal",
		})) as User;
		const lucia = initializeLucia(d1);
    await lucia.deleteExpiredSessions();
		const session = await lucia.createSession(d1Data.id, {});
		return {
			code: 200,
			session: session.id,
			user: {
				...d1Data,
				password_hash: "",
			},
		};
	}
};

export const verify = async (d1: D1Database, sessionId: string) => {
	const lucia = initializeLucia(d1);
	const { session, user } = await lucia.validateSession(sessionId);
	if (session && user) {
		return true;
	}
	return false;
};

declare module "lucia" {
	interface Register {
		Lucia: ReturnType<typeof initializeLucia>;
	}
}
